//Date: 2023-11-12 22:03:15
//LastEditors: jie zhengjie_liang@qq.com
//LastEditTime: 2023-11-26 00:20:37
//FilePath: /photowall/pkg/main/options/staticfile_opts.go

package options

import (
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"hash"
	"io"
	"mime/multipart"
	"os"
	"path/filepath"
	"photowall/pkg/config"
	"photowall/pkg/log"
	"photowall/pkg/objdef"
	"strconv"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
)

//description: 异步添加文件
//param {string} md5
//return {*}

func AsyncAddStaticFile(fileMetadata *objdef.FileMetadata, ctx *gin.Context) {
	if fileMetadata == nil {
		log.GetMainLogger().Error("nil pointer inputed")
		return
	}

	go asyncAddStaticFileToDB(fileMetadata, ctx)
}

//description: 如果数据库存在该文件，则增加引用，不用保存，否则调用asyncSaveFile进行更深的操作
//param {*objdef.FileMetadata} fileMetadata
//param {*gin.Context} ctx
//return {*}

func asyncAddStaticFileToDB(fileMetadata *objdef.FileMetadata, ctx *gin.Context) {
	exist := isFileExistInDB(fileMetadata.FileMD5)
	if exist {
		res := insertStaticFileToDB(fileMetadata)
		if !res {
			log.GetDatabaseLogger().Error("cat not add file: " + strconv.FormatBool(res))
			return
		}

		return
	}

	go asyncSaveFile(fileMetadata, ctx)

}

//description: 保存文件后根据保存结果，更新数据库和redis
//param {*objdef.FileMetadata} fileMetadata
//param {*gin.Context} ctx
//return {*}

func asyncSaveFile(fileMetadata *objdef.FileMetadata, ctx *gin.Context) {
	fileHeader, err := ctx.FormFile("file")
	if err != nil {
		log.GetMainLogger().Error(err.Error() + "get file header failed!")
		return
	}

	err = ctx.SaveUploadedFile(fileHeader,
		config.GetConfig().FileSaveConfig.ImageConfig.ImageSavePath+"/"+fileMetadata.FileMD5)
	if err != nil {
		log.GetMainLogger().Error(fmt.Sprintf("can not save file %v, the path is %v", fileHeader,
			config.GetConfig().FileSaveConfig.ImageConfig.ImageSavePath+"/"+fileMetadata.FileMD5))
	}

	insertStaticFileToDB(fileMetadata)

	putFileMetadataIntoRedis(fileMetadata)
}

var (
	md5Hash      hash.Hash
	md5SyncOnce  sync.Once
	md5HashMutex sync.Mutex
)

//description: 获取文件的md5
//param {*multipart.FileHeader} fileHeader
//return {*}

func GetMD5ByMultipartHeader(fileHeader *multipart.FileHeader) string {
	md5HashMutex.Lock()
	defer md5HashMutex.Unlock()

	file, err := fileHeader.Open()
	if err != nil {
		go log.GetMainLogger().Error(err.Error())
	}

	md5SyncOnce.Do(func() {
		md5Hash = md5.New()
	})

	buffer := make([]byte, 1024)
	defer md5Hash.Reset()

	for {
		n, err := file.Read(buffer)
		if err != nil && err != io.EOF {
			go log.GetMainLogger().Error(err.Error())
			return ""
		}
		if n == 0 {
			// 读取完毕
			break
		}

		md5Hash.Write(buffer)
	}

	hash := md5Hash.Sum(nil)
	md5Str := hex.EncodeToString(hash[:])

	return md5Str
}

//description: 首先从redis中查看是否有缓存，如果有，则返回，否则从数据库中查看是否存在该文件
//param {string} md5
//return {*}

func CheckFileIsAvailable(md5 string) (bool, error) {
	exist, err := GetRedisClient().Exists("staitcfile:" + md5).Result()
	if err != nil {
		_, err := GetRedisClient().Ping().Result()
		if err != nil {
			log.GetRedisLogger().Error("GetRedisClient is not avaliable, ping failed!" + err.Error())
			return false, err
		}

		log.GetRedisLogger().Info("can not find md5 in redis " + md5)
	}

	if exist == 1 {
		// 如果缓存中找到了md5
		locked, err := GetRedisClient().HGet("staitcfile:"+md5, "locked").Result()
		if err != nil {
			log.GetRedisLogger().Error(fmt.Sprintf("file md5 %v have no fields locked", md5))
		}

		if locked == "1" {
			log.GetDatabaseLogger().Error(fmt.Sprintf("file is locked :%v", md5))
			return false, nil
		}

		return true, nil
	}

	// 缓存不存在，使用数据库进行查找
	fileMetadata := getFileByMD5FromDatabase(md5)
	if fileMetadata == nil {
		// 数据库中不存在
		log.GetDatabaseLogger().Info(fmt.Sprintf("can not find md5 in db:%v", md5))
		return false, nil
	}

	if fileMetadata.CitationCount > 0 {
		// 在数据库找到了md5，且可引用，将其更新到redis缓存中
		err = GetRedisClient().HSet("staitcfile:"+md5, "citation_count", fileMetadata.CitationCount).Err()
		if err != nil {
			log.GetRedisLogger().Error(err.Error() + " add key failed: " + md5 + " " + strconv.Itoa(fileMetadata.CitationCount))
		}

		expiration := time.Duration(config.GetConfig().FileSaveConfig.ImageConfig.RedisExpireTime) * time.Second
		if err := GetRedisClient().Expire("staitcfile:"+md5, expiration).Err(); err != nil {
			log.GetRedisLogger().Error("Failed to set expiration time: %v", err)
		}

		return true, nil
	} else {
		// 该文件已不可引用，返回false
		return false, nil
	}
}

// description: 减少引用次数，如果为0则删除该file
// param {string} md5
// return {*}

func AsyncDeleteFile(md5 string) bool {
	err := GetRedisClient().HSet("staitcfile:"+md5, "locked", true).Err()
	if err != nil {
		log.GetRedisLogger().Error(err.Error() + fmt.Sprintf("can not set locked value %v", md5))
	}
	defer func(md5 string) {
		err = GetRedisClient().HSet("staitcfile:"+md5, "locked", false).Err()
		if err != nil {
			log.GetRedisLogger().Error(err.Error() + fmt.Sprintf("can not set locked value %v", md5))
		}
	}(md5)

	fileData := getFileByMD5FromDatabase(md5)
	if fileData == nil {
		return true
	}

	if fileData.CitationCount <= 1 {
		deleteStaticFileInDB(md5)
	} else {
		deleteStaticFileInDB(md5)
	}

	go deleteFileInFileSystem(md5)

	return true
}

//description: 删除文件系统中的文件
//param {string} md5
//return {*}

func deleteFileInFileSystem(md5 string) bool {
	filePath := filepath.Join(config.GetConfig().FileSaveConfig.ImageConfig.ImageSavePath, md5)

	fileInfo, err := os.Stat(filePath)
	if os.IsNotExist(err) {
		fmt.Println("文件不存在")
		return false
	} else if err != nil {
		fmt.Printf("检查文件时发生错误：%s\n", err.Error())
		return false
	}

	if fileInfo.IsDir() {
		log.GetMainLogger().Error("delete option should be a file, but a dir received")
		return false
	}

	if fileInfo.Mode().IsRegular() {
		err := os.Remove(filePath)
		if err != nil {
			log.GetMainLogger().Error(err.Error() + fmt.Sprintf("filepath is not a IsRegular file, %v", fileInfo))
		}

		return true
	}

	log.GetMainLogger().Error("deleteFileInFileSystem have other condition")

	return false
}

func GetStaticFilesNum() int64 {
	size, err := GetRedisClient().SCard(config.GetConfig().ImageConfig.RedisKey).Result()
	if err != nil {
		go log.GetRedisLogger().Error(err.Error())
		return -1
	}

	return size
}
